
#include <QSettings>
#include <QDateTime>
#include <QDir>
#include <stdexcept>
#include "sqlitethreadstorage.h"
#include "log.h"
#include "components/thread.h"
#include "components/board.h"


namespace Wishmaster {

SQLiteThreadStorage::SQLiteThreadStorage(const QString& root) :
	m_root(root), m_maxThreads(100)
{
	QDir r;
	if(!r.exists(root))
		r.mkpath(root);

	m_sqlite.open(QString(m_root + QDir::separator() + "threads.db").toUtf8().data());
	log(llDebug, "Thread database opened successfully (%s)", qPrintable(root));

	m_sqlite.execDML("CREATE TABLE IF NOT EXISTS threads (id INTEGER, host STRING, board STRING, thread_id INTEGER, thread_name STRING, flags INTEGER, postcount INTEGER, sortorder INTEGER, poster_name STRING, PRIMARY KEY(id))");
	m_sqlite.execDML("CREATE TABLE IF NOT EXISTS posts (id INTEGER, thread_id INTEGER, post_id INTEGER, topic STRING, text STRING, thumblink STRING, piclink STRING, timestamp INTEGER, flags INTEGER, PRIMARY KEY(id))");

	convertFrom060Format();
}

SQLiteThreadStorage::~SQLiteThreadStorage()
{
}

void SQLiteThreadStorage::convertFrom060Format()
{
	CppSQLite3Query query = m_sqlite.execQuery("SELECT * FROM posts");
	try
	{
		query.fieldIndex("poster_name");
	}
	catch(CppSQLite3Exception& e)
	{
		m_sqlite.execDML("ALTER TABLE posts ADD poster_name STRING");
	}

	query = m_sqlite.execQuery("SELECT * FROM threads");
	try
	{
		query.fieldIndex("leader_pic");
	}
	catch(CppSQLite3Exception& e)
	{
		m_sqlite.execDML("ALTER TABLE threads ADD leader_pic STRING");
	}
}

uint32_t SQLiteThreadStorage::makeFlags(Thread* thr)
{
	uint32_t flags = 0;
	if(thr->visibility() == Hide ||
	   thr->visibility() == HideForever)
		flags |= ThreadHidden;
	if(!thr->flag(Thread::Alive))
		flags |= ThreadDead;
	if(thr->flag(Thread::Archived))
		flags |= ThreadArchived;
	if(thr->flag(Thread::Responded))
		flags |= ThreadResponded;
	return flags;
}

void SQLiteThreadStorage::setFlags(ThreadPtr thr, uint32_t flags)
{
	if(flags & ThreadHidden)
		thr->setVisibility(Hide);
	if(flags & ThreadDead)
		thr->setFlag(Thread::Alive, false);
	if(flags & ThreadArchived)
		thr->setFlag(Thread::Archived, true);
	if(flags & ThreadResponded)
		thr->setFlag(Thread::Responded, true);
}

void SQLiteThreadStorage::setFlags(Thread* thr, uint32_t flags)
{
	if(flags & ThreadHidden)
		thr->setVisibility(Hide);
	if(flags & ThreadDead)
		thr->setFlag(Thread::Alive, false);
	if(flags & ThreadArchived)
		thr->setFlag(Thread::Archived, true);
	if(flags & ThreadResponded)
		thr->setFlag(Thread::Responded, true);
}

bool SQLiteThreadStorage::storeThread(const QString& host, const QString& board, Thread* thr)
{
	try
	{
		uint32_t flags = makeFlags(thr);

		ThreadEntry entry;
		entry.host = host;
		entry.board = board;
		entry.name = thr->getName();
		entry.thread_id = thr->id().threadNumericId();
		entry.flags = flags;
		entry.postcount = thr->getActualPostsCount();
		entry.sortorder = thr->getSortOrder();
		entry.leader_pic = saveLeaderPic(thr);
		unsigned long thread_entry_id = thread(host, board, thr->id().threadNumericId());
		if(thread_entry_id == InvalidThreadId)
			entry.id = 0;
		else
			entry.id = thread_entry_id;
		entry.id = setThread(entry);
		if(thr->visibility() == Hide ||
		   thr->visibility() == HideForever)
			return true;


		clearThread(entry.id);
		return storePosts(thr->parent(), thr, entry.id);
	}
	catch(CppSQLite3Exception& e)
	{
		log(llWarning, "SQLite exception: %s", e.errorMessage());
		return false;
	}
}

QString SQLiteThreadStorage::saveLeaderPic(Thread* thread)
{
	QSettings settings;
	QString format = settings.value("threadstorage/static_image_format", ".jpg").toString();
	QString threadDir = threadStorageDir(thread->id());
	QString fullThreadDir = m_root + QDir::separator() + threadDir;
	QString filename = threadDir + QDir::separator() + "leaderpic" + format;
	QDir dir(fullThreadDir);
	if(!dir.exists())
		dir.mkpath(".");
	QFileInfo file(filename);
	if(file.exists())
		return filename;

	QImage image = thread->leaderPic();
	log(llDebug, "Saving leaderpic: %s(%d/%d)", qPrintable(filename), image.size().width(), image.size().height());
	image.save(m_root + QDir::separator() + filename);
	return filename;
}

bool SQLiteThreadStorage::storePosts(Board* b, Thread* thr, unsigned long thread_entry_id)
{
	QSettings settings;
	QString format = settings.value("threadstorage/static_image_format", ".jpg").toString();
	QString host = b->getParent()->getHost();
	QString board = b->name();
	QString threadDir = threadStorageDir(host, board, thr->id().threadNumericId());
	QDir root(m_root);
	if(!root.exists(threadDir))
		root.mkpath(threadDir);

	//for(Thread::postIterator it = thr->visiblePosts()->begin(); it != thr->visiblePosts()->end(); ++it)
	for(int postNumber = 0; postNumber < thr->getActualPostsCount(); postNumber++)
	{
		PostPtr post = thr->getPost(postNumber);
		uint32_t flags = 0;
		if(post->visibility() == Hide ||
		   post->visibility() == HideForever)
			flags |= PostHidden;
		if(!addPost(thread_entry_id, post, flags))
		{
			log(llWarning, "Error when adding post: %d/%d", thread_entry_id, post->id().postId());
		}
	}
	log(llDebug, "SQLiteThreadStorage::storePosts done");
	return true;
}

QString SQLiteThreadStorage::makeThumbFilename(unsigned long post_id, int attachIndex,  QString format) const
{
	return QString::number(post_id) + "_thumb" + QString::number(attachIndex) + format;
}

QString SQLiteThreadStorage::makePicFilename(unsigned long post_id, QString format) const
{
	return QString::number(post_id) + format;
}

ThreadPtr SQLiteThreadStorage::restoreThread(Board* board, unsigned long thread_id, bool lrestorePosts)
{
	try
	{
		unsigned long thread_entry_id = thread(board->getParent()->getHost(), board->name(), thread_id);
		if(thread_entry_id == InvalidThreadId)
		{
			log(llError, "Invalid thread_entry_id");
			return NULL;
		}
		log(llDebug, "Loading thread: %d", thread_id);
		ThreadEntry entry = getThread(thread_entry_id);

		Thread* thr = new Thread(ThreadId(board->getParent()->getHost(), board->name(), thread_id), board);
		setFlags(thr, entry.flags);
		thr->setFlag(Thread::Swapped, true);
		thr->setPostsCount(entry.postcount);
		thr->setName(entry.name);
		thr->setSortOrder(entry.sortorder);
		thr->setLeaderPic(restoreLeaderPic(thr, entry.leader_pic));

		if(lrestorePosts)
		{
			restorePosts(board, thr, thread_entry_id);
			thr->setPostsCount(0);
			thr->setFlag(Thread::Swapped, false);
		}
		return ThreadPtr(thr);
	}
	catch(CppSQLite3Exception& e)
	{
		log(llWarning, "SQLite exception: %s", e.errorMessage());
		return ThreadPtr();
	}
}

QImage SQLiteThreadStorage::restoreLeaderPic(Thread* thread, const QString& leaderPic)
{
	QString filename = m_root + QDir::separator() + leaderPic;
	QImage image;
	image.load(filename);
	return image;
}

bool SQLiteThreadStorage::restoreThread(Board* board, unsigned long thread_id, Thread* thr)
{
	try
	{
		unsigned long thread_entry_id = thread(board->getParent()->getHost(), board->name(), thread_id);
		if(thread_entry_id == InvalidThreadId)
		{
			log(llError, "Invalid thread_entry_id");
			return false;
		}
		ThreadEntry entry = getThread(thread_entry_id);
		setFlags(thr, entry.flags);

		thr->setPostsCount(entry.postcount);
		thr->setName(entry.name);
		thr->setSortOrder(entry.sortorder);
		thr->setLeaderPic(restoreLeaderPic(thr, entry.leader_pic));
		log(llDebug, "(A)Loading thread: %d", thread_id);
		restorePosts(board, thr, thread_entry_id);

		return true;
	}
	catch(CppSQLite3Exception& e)
	{
		log(llWarning, "SQLite exception: %s", e.errorMessage());
		return false;
	}
}

void SQLiteThreadStorage::restorePosts(Board* board, Thread* thr, unsigned long thread_entry_id)
{
	QString threadDir = threadStorageDir(board->getParent()->getHost(), board->name(), thr->id().threadNumericId());
	CppSQLite3Query q = m_sqlite.execQuery(QString("SELECT * FROM posts WHERE thread_id='%1'").arg(thread_entry_id).toUtf8().data());
	while(!q.eof())
	{
		unsigned long post_id = q.getIntField(2);
		const char* topic = q.getStringField(3);
		const char* text = q.getStringField(4);
		//const char* thumblink = q.getStringField(5);
		const char* attachments = q.getStringField(6);
		//unsigned long timestamp = q.getIntField(7);
		uint32_t flags = q.getIntField(8);
		QString postername = QString::fromUtf8(q.getStringField(9));
		PostPtr post = PostPtr(new Post(PostId(thr->id(), post_id)));
		post->setTopic(QString::fromUtf8((const char*)topic));
		post->setText(QString::fromUtf8((const char*)text));
		post->setName(postername);
		restoreAttachments(post, QString(attachments));
		if(flags & PostHidden)
			post->setVisibility(Hide);

		thr->appendPost(post);

		if(post->visibility() != Hide &&
		   post->visibility() != HideForever)
		{
			QList<PostId> links = board->parser()->findLinks(thr->id(), post->text());
			foreach(const PostId& link, links)
			{
				PostPtr p = thr->lookupPost(link);
				if(p)
					p->addReply(post->id());
			}
		}

		q.nextRow();
	}
}

unsigned long SQLiteThreadStorage::thread(const QString& host, const QString& board, unsigned long thread_id)
{
	try
	{
		QString req = QString("SELECT id FROM threads WHERE host='%1' AND board='%2' AND thread_id='%3'").arg(host).arg(board).arg(thread_id);
		CppSQLite3Query q = m_sqlite.execQuery(req.toUtf8().data());
		if(q.eof())
			return InvalidThreadId;
		return q.getIntField(0);
	}
	catch(CppSQLite3Exception& e)
	{
		log(llWarning, "SQLite exception: %s", e.errorMessage());
		return 0;
	}
}

unsigned long SQLiteThreadStorage::setThread(const ThreadEntry& entry)
{
	try
	{
		QString threadname = entry.name;
		QString sql;
		threadname.replace("'", "''");
		if(entry.id)
			sql = QString("INSERT OR REPLACE INTO threads (id, host, board, thread_id, thread_name, flags, postcount, sortorder, leader_pic) VALUES('%1', '%2', '%3', '%4', '%5', '%6', '%7', '%8', '%9')").
				arg(entry.id).
				arg(entry.host).
				arg(entry.board).
				arg(entry.thread_id).
				arg(threadname.left(100)).
				arg(entry.flags).
				arg(entry.postcount).
				arg(entry.sortorder).
				arg(entry.leader_pic);
		else
			sql = QString("INSERT OR REPLACE INTO threads (host, board, thread_id, thread_name, flags, postcount, sortorder, leader_pic) VALUES('%1', '%2', '%3', '%4', '%5', '%6', '%7', '%8')").
				arg(entry.host).
				arg(entry.board).
				arg(entry.thread_id).
				arg(threadname.left(100)).
				arg(entry.flags).
				arg(entry.postcount).
				arg(entry.sortorder).
				arg(entry.leader_pic);

		m_sqlite.execDML(sql.toUtf8().data());
		return m_sqlite.lastRowId();
	}
	catch(CppSQLite3Exception& e)
	{
		log(llWarning, "SQLite exception: %s", e.errorMessage());
		return 0;
	}
}

ThreadEntry SQLiteThreadStorage::getThread(unsigned long thread_entry_id)
{
	try
	{
		ThreadEntry entry;
		entry.id = entry.thread_id = entry.flags = entry.postcount = entry.sortorder = 0;
		QString req = QString("SELECT * FROM threads WHERE id='%1'").arg(thread_entry_id);
		CppSQLite3Query q = m_sqlite.execQuery(req.toUtf8().data());
		if(!q.eof())
		{
			entry.id = q.getIntField("id");
			entry.host = QString::fromUtf8(q.getStringField("host"));
			entry.board = QString::fromUtf8(q.getStringField("board"));
			entry.thread_id = q.getIntField("thread_id");
			entry.name = QString::fromUtf8(q.getStringField("thread_name"));
			entry.flags = q.getIntField("flags");
			entry.postcount = q.getIntField("postcount");
			entry.leader_pic = QString::fromUtf8(q.getStringField("leader_pic"));
		}
		return entry;
	}
	catch(CppSQLite3Exception& e)
	{
		log(llWarning, "SQLite exception: %s", e.errorMessage());
		return ThreadEntry();
	}
}

bool SQLiteThreadStorage::addPost(unsigned long thread_entry_id, PostPtr post, uint32_t flags)
{
	QString posttext = post->text();
	posttext.replace("'", "''");
	QString sql = QString("INSERT OR REPLACE INTO posts (thread_id, post_id, topic, text, thumblink, piclink, timestamp, flags, poster_name) VALUES('%1', '%2', '%3', '%4', '%5', '%6', '%7', '%8', '%9')").
				  arg(thread_entry_id).arg(post->id().postId()).arg(post->topic()).arg(posttext).arg(QString()).
				  arg(saveAttachments(post)).arg(QDateTime::currentDateTime().toTime_t()).arg(flags).arg(post->name());
	sql = sql.replace("\\", "\\\\");
	m_sqlite.execDML(sql.toUtf8().data());

	return true;
}

QList<unsigned long> SQLiteThreadStorage::threadList(const QString& host, const QString& board)
{
	QString req = QString("SELECT thread_id FROM threads WHERE host='%1' AND board='%2' ORDER BY sortorder DESC").arg(host).arg(board);
	CppSQLite3Query q = m_sqlite.execQuery(req.toUtf8().data());
	QList<unsigned long> list;

	while(!q.eof())
	{
		list.append(q.getIntField(0));
		q.nextRow();
	}
	return list;
}

QList<unsigned long> SQLiteThreadStorage::threadList(const QString& host, const QString& board, uint32_t flags)
{
	QString req = QString("SELECT thread_id FROM threads WHERE host='%1' AND board='%2' AND (flags & '%3')!=0 ORDER BY sortorder DESC").arg(host).arg(board).arg(flags);
	CppSQLite3Query q = m_sqlite.execQuery(req.toUtf8().data());
	QList<unsigned long> list;

	while(!q.eof())
	{
		list.append(q.getIntField(0));
		q.nextRow();
	}
	return list;
}

QString SQLiteThreadStorage::threadStorageDir(const QString& host, const QString& board, unsigned long thread_id)
{
	QDir root(m_root);
	QString threadDir = host + QDir::separator() + board + QDir::separator() + QString::number(thread_id);
	if(!root.exists(threadDir))
		root.mkpath(threadDir);
	log(llDebug, "threadStorageDir: %s", qPrintable(threadDir));
	threadDir = QDir::toNativeSeparators(threadDir);
	return threadDir;
}

QString SQLiteThreadStorage::threadStorageDir(const ThreadId& threadId)
{
	QDir root(m_root);
	QString threadDir = threadId.chanName() + QDir::separator() + threadId.boardId() + QDir::separator() + threadId.threadStringId();
	if(!root.exists(threadDir))
		root.mkpath(threadDir);
	threadDir = QDir::toNativeSeparators(threadDir);
	return threadDir;
}

QString SQLiteThreadStorage::threadStorageDir(const PostId& postId)
{
	return threadStorageDir(postId.chan(), postId.board(), postId.threadId());
}

QString SQLiteThreadStorage::thumb(const QString& host, const QString& board, unsigned long thread_id, unsigned long post_id)
{
	// TODO db request should be performed here
	QString str = threadStorageDir(host, board, thread_id);
	return str + QDir::separator() + QString::number(post_id) + ".png";
}

bool SQLiteThreadStorage::postExists(const QString& host, const QString& board, unsigned long thread_id, unsigned long post_id)
{
	QString req = QString("SELECT id FROM posts WHERE thread_id='%1' AND post_id='%2'").arg(thread_id).arg(post_id);
	CppSQLite3Query q = m_sqlite.execQuery(req.toUtf8().data());
	if(!q.eof())
		return true;
	else
		return false;
}

QString SQLiteThreadStorage::threadName(unsigned long thread_entry_id)
{
	QString req = QString("SELECT thread_name FROM threads WHERE id='%1'").arg(thread_entry_id);
	CppSQLite3Query q = m_sqlite.execQuery(req.toUtf8().data());
	if(!q.eof())
	{
		return QString::fromUtf8(q.getStringField(0));
	}
	else
		return QString::null;
}

void SQLiteThreadStorage::clearThread(unsigned long thread_entry_id)
{
	QString sql = QString("DELETE FROM posts WHERE thread_id='%1'").arg(thread_entry_id);
	m_sqlite.execDML(sql.toUtf8().data());
}

QList<unsigned long> SQLiteThreadStorage::deleteThreadsBySql(const QString& sql)
{
	CppSQLite3Query q = m_sqlite.execQuery(sql.toUtf8().data());
	for(int i = 0; i < m_maxThreads; i++)
	{
		if(q.eof()) // Nothing to delete
			return QList<unsigned long>();
		q.nextRow();
	}
	QList<unsigned long> list;
	QList<unsigned long> ids;
	while(!q.eof())
	{
		unsigned long id = q.getIntField("id");
		list.append(id);
		ids.append(q.getIntField("thread_id"));
		q.nextRow();
	}
	foreach(unsigned long id, list)
	{
		deleteThread(id);
	}
	return ids;
}

QList<unsigned long> SQLiteThreadStorage::deleteOldThreads(const QString& host, const QString& board)
{
	// First we delete hidden thread entries
	QList<unsigned long> ids;
	QString req = QString("SELECT * FROM threads WHERE host='%1' \
			AND board='%2' \
			AND (flags & 4)=0 \
			AND (flags & 2)!=0 \
			ORDER BY sortorder DESC").arg(host).arg(board);
	ids.append(deleteThreadsBySql(req));

	// And then, we delete non-hidden threads
	req = QString("SELECT * FROM threads WHERE host='%1' \
			AND board='%2' \
			AND (flags & 4)=0 \
			AND (flags & 2)=0 \
			ORDER BY sortorder DESC").arg(host).arg(board);
	ids.append(deleteThreadsBySql(req));
	return ids;
}

void SQLiteThreadStorage::deleteThread(unsigned long id)
{
	ThreadEntry entry = getThread(id);
	QDir threadDir(m_root + QDir::separator() + threadStorageDir(entry.host, entry.board, entry.thread_id));
	foreach(QString s, threadDir.entryList())
	{
		threadDir.remove(s);
	}
	QString dirname = threadDir.dirName();
	threadDir.cdUp();
	threadDir.rmdir(dirname);
	m_sqlite.execDML(QString("DELETE FROM posts WHERE thread_id='%1'").arg(id).toUtf8().data());
	QString sql = QString("DELETE FROM threads WHERE id='%1'").arg(id);
	m_sqlite.execDML(sql.toUtf8().data());
}

void SQLiteThreadStorage::setMaxThreads(int max)
{
	m_maxThreads = max;
}

QString SQLiteThreadStorage::saveAttachments(PostPtr& post)
{
	QString threadDir = threadStorageDir(post->id());
	QString attachments;
	for(int i = 0; i < post->attachmentCount(); i++)
	{
		QString fname = post->attachment(i)->url().path();
		fname = fname.mid(fname.lastIndexOf('/') + 1);
		QUrl thumburl = QUrl::fromLocalFile(QString("//") + threadDir + QDir::separator() + makeThumbFilename(post->id().postId(), i, ".jpg"));
		QUrl fileurl = QUrl::fromLocalFile(QString("//") + threadDir + QDir::separator() + fname);


		QImage thumb = post->attachment(i)->thumbnail();
		thumb.save(m_root + QDir::separator() + thumburl.toLocalFile());

		if(!post->attachment(i)->data().isEmpty())
		{
			QFile f(m_root + QDir::separator() + fileurl.toLocalFile());
			if(f.open(QIODevice::WriteOnly | QIODevice::Truncate))
			{
				f.write(post->attachment(i)->data());
				f.close();
			}
		}
		else
		{
			fileurl = post->attachment(i)->url();
		}
		//log(llDebug, "Saving: %s/%s", qPrintable(thumburl.toString()), qPrintable(fileurl.toString()));
		attachments.append(QString("%1|%2!").arg(thumburl.toString(), fileurl.toString()));
	}
	return attachments;
}

void SQLiteThreadStorage::restoreAttachments(PostPtr& post, const QString& attachments)
{
	QString threadDir = threadStorageDir(post->id());
	QStringList attachs = attachments.split('!', QString::SkipEmptyParts);
	int index = 0;
	foreach(const QString& attach, attachs)
	{
		QUrl thumburl = QUrl(attach.section('|', 0, 0));
		QUrl fileurl = QUrl(attach.section('|', 1, 1));
		//log(llDebug, "Loading: %s/%s", qPrintable(thumburl.toString()), qPrintable(fileurl.toString()));
		AttachmentPtr attachment = Attachment::create(thumburl, fileurl);

		QImage thumb;
		thumb.load(m_root + QDir::separator() + thumburl.toLocalFile());
		attachment->setThumbnail(thumb);

		if(fileurl.scheme() == "file")
		{
			//log(llDebug, "Restoring: %s", qPrintable(fileurl.toString()));
			QFile f(m_root + QDir::separator() + fileurl.toLocalFile());
			if(f.open(QIODevice::ReadOnly))
			{
				//log(llDebug, "Restoring: %s", qPrintable(f.fileName()));
				attachment->setData(f.readAll());
				f.close();
			}
		}
		post->addAttachment(attachment);
		index++;
	}
}

} // namespace Wishmaster
